package org.yinxue.framework.jdbc.datasource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yinxue.framework.jdbc.constant.JdbcConstant;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 数据库连接池 <br>
 * 采用数组+线程私有变量的简单实现
 *
 * @author zengjian
 * @create 2018-03-15 9:20
 * @since 1.0.0
 */
public class YXDataSource implements DataSource, JdbcConstant, YXDataSourceMBean {

    private static final Logger LOGGER = LoggerFactory.getLogger(YXDataSource.class);

    private static final long DEFAULT_CONNECT_TIME = TimeUnit.SECONDS.toMillis(20);
    private static final long DEFAULT_IDLE_TIME = TimeUnit.MINUTES.toMillis(180);
    private static final int DEFAULT_CORE_SIZE = 10;

    private String username;
    private String password;
    private String url;
    private String driverName;
    private Properties propeties;

    private ThreadLocal<YXConnection> local = new ThreadLocal<YXConnection>();
    private YXConnection[] connections;
    /**
     * maxActive - initialSize
     */
    private YXConnection[] extraConnections;
    private volatile int size;
    private volatile int extraSize;

    private Integer initialSize = DEFAULT_CORE_SIZE;
    private Integer maxActive = DEFAULT_CORE_SIZE;
    private long maxWait = DEFAULT_CONNECT_TIME;
    private long maxIdle = DEFAULT_IDLE_TIME;
    private long minIdle = DEFAULT_IDLE_TIME;

    private volatile boolean isPooled = false;
    private volatile boolean isExtraPooled = false;
    private ReentrantLock createLock = new ReentrantLock();
    private ReentrantLock extraCreateLock = new ReentrantLock();


    public YXDataSource() {

    }

    public YXDataSource(final Properties propeties) {
        this.propeties = propeties;
        unWrap(propeties);
        initDriver();
    }

    private void unWrap(Properties propeties) {
        this.username = (String) propeties.get("jdbc.username");
        this.password = (String) propeties.get("jdbc.password");
        this.driverName = (String) propeties.get("jdbc.driver");
        this.url = (String) propeties.get("jdbc.url");
        if (propeties.get("jdbc.initialSize") != null) {
            this.initialSize = Integer.parseInt((String) propeties.get("jdbc.initialSize"));
        }
        if (propeties.get("jdbc.maxActive") != null) {
            this.maxActive = Integer.parseInt((String) propeties.get("jdbc.maxActive"));
        }
        if (propeties.get("jdbc.minIdle") != null) {
            this.minIdle = Long.parseLong((String) propeties.get("jdbc.minIdle"));
        }
        if (propeties.get("jdbc.maxIdle") != null) {
            this.maxIdle = Long.parseLong((String) propeties.get("jdbc.maxIdle"));
        }
        String maxWait = (String) propeties.get("jdbc.maxWait");
        if (maxWait != null && !"0".equals(maxWait)) {
            this.maxWait = Long.parseLong((String) propeties.get("jdbc.maxWait"));
        }
        initDriver();
    }

    public YXDataSource(final String username, final String password, final String url, final String driverName) {
        this.username = username;
        this.password = password;
        this.url = url;
        this.driverName = driverName;
        initDriver();
    }

    private void initDriver() {
        try {
            Class.forName(this.driverName);
        } catch (ClassNotFoundException e) {
            LOGGER.error("未找到对应的驱动:{}", this.driverName);
        }
    }

    @Override
    public Connection getConnection() throws SQLException {
        long startTime = System.currentTimeMillis();
        if (!isPooled) {
            initPool();
        }
        // 同一线程中获取
        if (local.get() != null) {
            YXConnection con = local.get();
            if (con.compareAndSet(NO_USE, USING)) {
                if (checkTimeout(startTime, con)) {
                    return con;
                }
            }
        }
        // 连接池中获取
        for (; ; ) {
            if (connections != null) {
                //  循环一次核心池获取
                for (int i = 0; i < size; i++) {
                    YXConnection con = connections[i];
                    if (con.compareAndSet(NO_USE, USING)) {
                        if (checkTimeout(startTime, con)) {
                            return con;
                        }
                    }
                }
                // 循环一次额外池
                if (isExtraPooled == false) {
                    initExtraPool();
                }
                for (int i = 0; i < extraSize; i++) {
                    YXConnection con = extraConnections[i];
                    if (con.compareAndSet(NO_USE, USING)) {
                        if (checkTimeout(startTime, con)) {
                            return con;
                        }
                    }
                }
                checkTimeoutOnly(startTime);
            }

        }
    }

    private void checkTimeoutOnly(long startTime) throws SQLException {
        long endTime = System.currentTimeMillis();
        if (endTime - startTime >= maxWait) {
            throw new SQLException("获取连接超时");
        }
    }

    private boolean checkTimeout(long startTime, YXConnection con) throws SQLException {
        long endTime = System.currentTimeMillis();
        if (endTime - startTime < maxWait) {
            con.setLastActiveTime(System.currentTimeMillis());
            local.set(con);
            return true;
        } else {
            resetAndException(con);
        }
        return false;
    }

    private void initExtraPool() throws SQLException {
        try {
            extraCreateLock.lock();
            if (!isExtraPooled) {
                int extraCount = maxActive - initialSize;
                if (extraConnections == null) {
                    extraConnections = new YXConnection[extraCount];
                }
                for (int i = 0; i < extraCount; i++) {
                    YXConnection con = this.createConnection();
                    extraConnections[extraSize++] = con;
                }
                isExtraPooled = true;
            }
        } finally {
            extraCreateLock.unlock();
        }
    }

    private void resetAndException(YXConnection con) throws SQLException {
        con.compareAndSet(USING, NO_USE);
        throw new SQLException("获取连接超时");
    }

    private void initPool() throws SQLException {
        try {
            createLock.lock();
            // dbcheck
            if (!isPooled) {
                if (connections == null) {
                    connections = new YXConnection[initialSize];
                }
                for (int i = 0; i < initialSize; i++) {
                    YXConnection con = createConnection();
                    connections[size++] = con;
                }
                registerMBean();
                isPooled = true;
            }
        } finally {
            createLock.unlock();
        }
    }

    private void registerMBean() {
        try {
            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
            ObjectName yaDataSource = new ObjectName("jmxBean:name=YXDataSource");
            server.registerMBean(this, yaDataSource);
        } catch (Exception e) {
            throw new RuntimeException("YADataSource注册MBean异常");
        }
    }

    private YXConnection createConnection() throws SQLException {
        YXConnection con = null;
        Connection realCon = DriverManager.getConnection(getUrl(), getUsername(), getPassword());
        con = new YXConnection(this, realCon);
        con.setCreateTime(System.currentTimeMillis());
        return con;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new UnsupportedOperationException();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String getUrl() {
        return url;
    }

    @Override
    public void setUrl(String url) {
        this.url = url;
    }

    @Override
    public String getDriverName() {
        return driverName;
    }

    @Override
    public void setDriverName(String driverName) {
        this.driverName = driverName;
    }

    @Override
    public Properties getPropeties() {
        return propeties;
    }

    @Override
    public void setPropeties(Properties propeties) {
        this.propeties = propeties;
    }

    @Override
    public Integer getInitialSize() {
        return initialSize;
    }

    @Override
    public void setInitialSize(Integer initialSize) {
        this.initialSize = initialSize;
    }

    @Override
    public Integer getMaxActive() {
        return maxActive;
    }

    @Override
    public void setMaxActive(Integer maxActive) {
        this.maxActive = maxActive;
    }

    @Override
    public long getMaxWait() {
        return maxWait;
    }

    @Override
    public void setMaxWait(long maxWait) {
        this.maxWait = maxWait;
    }

    @Override
    public long getMaxIdle() {
        return maxIdle;
    }

    @Override
    public void setMaxIdle(long maxIdle) {
        this.maxIdle = maxIdle;
    }

    @Override
    public long getMinIdle() {
        return minIdle;
    }

    @Override
    public void setMinIdle(long minIdle) {
        this.minIdle = minIdle;
    }

    @Override
    public ThreadLocal<YXConnection> getLocal() {
        return local;
    }

    @Override
    public void setLocal(ThreadLocal<YXConnection> local) {
        this.local = local;
    }

    @Override
    public YXConnection[] getConnections() {
        return connections;
    }

    @Override
    public void setConnections(YXConnection[] connections) {
        this.connections = connections;
    }

    @Override
    public YXConnection[] getExtraConnections() {
        return extraConnections;
    }

    @Override
    public void setExtraConnections(YXConnection[] extraConnections) {
        this.extraConnections = extraConnections;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public void setSize(int size) {
        this.size = size;
    }

    @Override
    public int getExtraSize() {
        return extraSize;
    }

    @Override
    public void setExtraSize(int extraSize) {
        this.extraSize = extraSize;
    }

    @Override
    public boolean isPooled() {
        return isPooled;
    }

    @Override
    public void setPooled(boolean pooled) {
        isPooled = pooled;
    }

    @Override
    public boolean isExtraPooled() {
        return isExtraPooled;
    }

    @Override
    public void setExtraPooled(boolean extraPooled) {
        isExtraPooled = extraPooled;
    }

    @Override
    public ReentrantLock getCreateLock() {
        return createLock;
    }

    @Override
    public void setCreateLock(ReentrantLock createLock) {
        this.createLock = createLock;
    }

    @Override
    public ReentrantLock getExtraCreateLock() {
        return extraCreateLock;
    }

    @Override
    public void setExtraCreateLock(ReentrantLock extraCreateLock) {
        this.extraCreateLock = extraCreateLock;
    }
}
